package org.dromara.web.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.PasswordLoginBody;
import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.user.CaptchaException;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Service;

/**
 * 密码认证策略
 *
 * @author Michelle.Chung
 */
@Slf4j
@Service("password" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class PasswordAuthStrategy implements IAuthStrategy {

    private final CaptchaProperties captchaProperties;
    private final SysLoginService loginService;
    private final SysUserMapper userMapper;

    /**
     * 执行用户登录操作
     *
     * @param body   登录对象
     * @param client 授权管理视图对象
     * @return 登录验证信息
     */
    @Override
    public LoginVo login(String body, SysClientVo client) {
        log.info("【密码登录认证】执行用户登录操作, body：{}，client：{}", body, JsonUtils.toJsonString(client));

        // 解析用户登录接口请求参数信息
        PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);

        // 校验请求参数
        ValidatorUtils.validate(loginBody);

        // 租户ID
        String tenantId = loginBody.getTenantId();
        // 用户名
        String username = loginBody.getUsername();
        // 密码
        String password = loginBody.getPassword();
        // 验证码
        String code = loginBody.getCode();
        // uuid唯一标识
        String uuid = loginBody.getUuid();

        // 校验是否开启验证码
        boolean captchaEnabled = captchaProperties.getEnable();
        if (captchaEnabled) {
            // 校验验证码
            validateCaptcha(tenantId, username, code, uuid);
        }

        // 校验用户信息
        LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
            SysUserVo user = loadUserByUsername(username);
            log.info("【密码登录认证】执行用户登录操作, user：{}", JsonUtils.toJsonString(user));
            // 校验用户登录信息
            loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
            // 此处可根据登录用户的数据不同 自行创建 loginUser
            return loginService.buildLoginUser(user);
        });

        // 客户端key
        loginUser.setClientKey(client.getClientKey());
        // 设备类型
        loginUser.setDeviceType(client.getDeviceType());

        log.info("【密码登录认证】执行用户登录操作, loginUser：{}", JsonUtils.toJsonString(loginUser));

        // 构建Sa登录参数
        SaLoginParameter model = new SaLoginParameter();
        // 设置设备类型
        model.setDeviceType(client.getDeviceType());
        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
        // 例如: 后台用户30分钟过期 app用户1天过期
        model.setTimeout(client.getTimeout());
        // token活跃超时时间
        model.setActiveTimeout(client.getActiveTimeout());
        // 设置扩展信息
        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
        // 生成token
        LoginHelper.login(loginUser, model);
        log.info("【密码登录认证】执行用户登录操作成功, model：{}", JsonUtils.toJsonString(model));

        // 构建用户登录验证信息
        LoginVo loginVo = new LoginVo();
        loginVo.setAccessToken(StpUtil.getTokenValue());
        loginVo.setExpireIn(StpUtil.getTokenTimeout());
        loginVo.setClientId(client.getClientId());
        log.info("【密码登录认证】执行用户登录操作成功, loginVo：{}", JsonUtils.toJsonString(loginVo));
        return loginVo;
    }

    /**
     * 校验验证码
     *
     * @param username 用户名
     * @param code     验证码
     * @param uuid     唯一标识
     */
    private void validateCaptcha(String tenantId, String username, String code, String uuid) {
        log.info("【密码登录认证】执行用户登录操作, 校验验证码, username：{}，code：{}，uuid：{}", username, code, uuid);

        // 构造Redis中存储验证码的键名，使用全局常量作为前缀加上uuid
        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");

        // 从Redis中获取缓存的验证码
        String captcha = RedisUtils.getCacheObject(verifyKey);

        // 验证完成后立即删除该验证码，防止重复使用
        RedisUtils.deleteObject(verifyKey);

        // 如果验证码不存在或已过期
        if (StringUtils.isBlank(captcha)) {
            // 记录登录失败的日志信息
            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));

            // 抛出验证码过期异常
            log.error("【密码登录认证】验证码过期, uuid：{}，code：{}", uuid, code);
            throw new CaptchaExpireException();
        }

        // 比较用户输入的验证码与缓存中的验证码（忽略大小写）
        if (!code.equalsIgnoreCase(captcha)) {
            // 记录登录失败的日志信息
            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));

            // 抛出验证码错误异常
            log.error("【密码登录认证】验证码错误, uuid：{}，code：{}", uuid, code);
            throw new CaptchaException();
        }
    }

    /**
     * 根据用户名加载用户信息
     *
     * @param username 用户名
     * @return 用户信息
     */
    private SysUserVo loadUserByUsername(String username) {
        log.info("【密码登录认证】执行用户登录操作, 根据用户名加载用户信息, username：{}", username);

        if (StringUtils.isBlank(username)) {
            log.error("【密码登录认证】执行用户登录操作, 用户名不能为空！");
            throw new UserException("user.not.exists", username);
        }

        // 根据条件查询单个VO对象
        SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));

        // 判断用户是否为空
        if (ObjectUtil.isNull(user)) {
            log.error("登录用户：{} 不存在.", username);
            throw new UserException("user.not.exists", username);
        } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
            log.error("登录用户：{} 已被停用.", username);
            throw new UserException("user.blocked", username);
        }
        return user;
    }

}
